﻿using System;
using System.IO;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ThreadState = System.Threading.ThreadState;

namespace ExcelRefresh.Excel
{
    public class ExcelRefreshEngine : IDisposable
    {
        private static readonly TimeSpan EmptyQueueSleepDuration = new TimeSpan(0, 0, 3);
        private readonly TimeSpan _refreshTimeout;
        private readonly Thread[] _threads;
        private readonly List<FileToRefresh> _inProgress;
        private readonly IProducerConsumerCollection<FileToRefresh> _files;
        private readonly IProducerConsumerCollection<FileToRefresh> _nextFiles;
        private readonly IProducerConsumerCollection<FileToRefresh> _results;
        private bool _isDisposing;

        public ExcelRefreshEngine(int numberOfThreads, TimeSpan refreshTimeout, IProducerConsumerCollection<FileToRefresh> files)
        {
            _files = files;
            _refreshTimeout = refreshTimeout;
            _inProgress = new List<FileToRefresh>();
            _results = new ConcurrentQueue<FileToRefresh>();
            _nextFiles = new ConcurrentQueue<FileToRefresh>();

            _threads = Enumerable
                .Range(0, numberOfThreads)
                .Select(i => new Thread(QueueProcessorLoop) { IsBackground = false, Priority = ThreadPriority.AboveNormal })
                .ToArray();

            Array.ForEach(_threads, t => t.Start());
        }
        
        public void Dispose()
        {
            if (_isDisposing) return;
            _isDisposing = true;

            var sessionID = Process.GetCurrentProcess().SessionId;

            while (_threads.Any(thread => thread.IsAlive))
            {
                foreach (var thread in _threads.Where(thread => thread.IsAlive))
                    thread.Abort();

                foreach (var process in Process.GetProcesses())
                    try
                    {
                        if (!process.HasExited &&
                            process.SessionId == sessionID &&
                            process.ProcessName.ToLower() == "excel"
                            )
                            process.Kill();
                    }
                    catch { }        // Usuall an Access Denied exception
            }
        }

        private void QueueProcessorLoop()
        {
            while (true)
            {
                FileToRefresh file;

                if (_files.TryTake(out file))
                    ProcessFile(file);
                else
                    Thread.Sleep(EmptyQueueSleepDuration);

                if (_isDisposing)
                    return;
            }
        }

        private void ProcessFile(FileToRefresh file)
        {
            // Discard the file if it's already in progress.
            lock (_inProgress)
            {
                if (_inProgress.Any(busy => busy.FilePath == file.FilePath))
                    return;
            }

            lock (_results)
            {
                if (_results.Any(done => done.FilePath == file.FilePath && !file.IsStale))
                    return;
            }

            var workerThread = new Thread(fi => ProcessFileDoWork((FileToRefresh)file));
            try
            {
                workerThread.TrySetApartmentState(ApartmentState.STA);
                workerThread.IsBackground = false;
                workerThread.Priority = ThreadPriority.AboveNormal;
                workerThread.Start(file);
                workerThread.Join(_refreshTimeout);
            }
            finally
            {
                if (workerThread.IsAlive)
                {
                    workerThread.Abort();
                    workerThread.Join();
                }
            }
        }

        /// <summary>
        /// Runs a thread and processes an Excel file.
        /// </summary>
        /// <param name="fileInfo"></param>
        private void ProcessFileDoWork(FileToRefresh file)
        {
            try
            {
                try
                {
                    lock (_inProgress)
                        _inProgress.Add(file);

                    file.Attempt++;
                    file.StartTime = DateTime.Now;
                    ExcelHelpers.RefreshAllWorkbookData(file);
                    file.EndTime = DateTime.Now;
                    _results.TryAdd(file);
                }
                finally
                {
                    file.EndTime = DateTime.Now;

                    lock (_inProgress)
                        _inProgress.Remove(file);
                }
            }
            catch (ThreadAbortException ex)
            {
                file.ExceptionThrown = ex;
                _results.TryAdd(file);
                throw;
            }
            catch (ExcelRefresh.Com.TooManyPowerPivotFilesException ex)
            {
                file.ExceptionThrown = ex;

                if (file.Attempt < 3)
                {
                    var timer = new Timer(self => { _files.TryAdd(file); ((Timer)self).Dispose(); });
                    timer.Change(new TimeSpan(0, 25, 0),             // wait 25 minutes
                                 TimeSpan.FromMilliseconds(-1));     // don't repeat
                    
                    _files.TryAdd(file);
                }
                else
                {
                    file.ExceptionThrown = ex;
                    _results.TryAdd(file);
                    throw;
                }
            }
            catch (Exception ex)
            {
                file.ExceptionThrown = ex;
                _results.TryAdd(file);
            }            
        }
    }
}